# Standard library imports
import importlib.util
import json
import os
import sys
import threading
import time

# Third-party imports
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger

# Local imports
from Tools.logConfig import setup_logging

# Constants
CONFIG_DIR = 'config'  # 配置文件目录
TASKS_DIR = 'tasks'  # 任务脚本目录
LOGS_DIR = os.path.join('logs', 'scheduler')  # 日志文件目录
LOG_FILE = 'scheduler.log'  # 日志文件名
CONFIG_FILE = 'taskConfig.json'  # 配置文件名称
CONFIG_CHECK_INTERVAL = 30  # 配置文件检查间隔时间（秒）
TASK_FILE = 'main.py'  # 任务入口文件名
TASK_REQUIREMENTS_FILE = 'requirements.txt'  # 依赖包文件名

# Global variables
scheduler = BackgroundScheduler()  # 调度器实例
logger = setup_logging("scheduler", LOGS_DIR)  # 日志记录器
config = {}  # 配置数据
hostName = ""  # 新增全局变量 hostName
_ip_geo_cache = None  # 新增IP地址信息全局变量
_last_cache_time = None  # 新增IP缓存时间全局变量


def load_config() -> dict:
    """Load task configuration from JSON file.

    Returns:
        dict: Configuration dictionary.

    Raises:
        FileNotFoundError: If config file is not found.
        json.JSONDecodeError: If config file contains invalid JSON.
    """
    try:
        # 获取配置文件路径
        config_path = os.path.join(CONFIG_DIR, CONFIG_FILE)
        # 读取并解析配置文件
        with open(config_path, 'r', encoding='utf-8') as file:
            global hostName  # 更新全局变量 hostName
            config_data = json.load(file)
            hostName = config_data.get('hostName', '')
            return config_data
    except Exception as e:
        _send_error_notification("load_config", str(e))
        logger.error(f"Failed to load config: {e}")
        raise


def load_task_function(task_name: str) -> callable:
    print(f"=====pyTaskScheduler====\nTask {task_name} is running!\n")
    """Load and initialize task function from task module.

    Args:
        task_name (str): Name of the task to load.

    Returns:
        callable: Wrapped task function.

    Raises:
        FileNotFoundError: If task file is not found.
        AttributeError: If task function is not found in module.
    """
    try:
        task_path = os.path.join(TASKS_DIR, task_name, f"{TASK_FILE}")
        if not os.path.exists(task_path):
            raise FileNotFoundError(f"Task file {task_path} not found")

        # Install task dependencies if requirements file exists
        requirements_path = os.path.join(TASKS_DIR, task_name, f"{TASK_REQUIREMENTS_FILE}")
        if os.path.exists(requirements_path):
            _install_dependencies(task_name, requirements_path)

        # Load task module
        spec = importlib.util.spec_from_file_location(task_name, task_path)
        task_module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(task_module)

        if not hasattr(task_module, 'task_function'):
            raise AttributeError(f"Task {task_name} does not have a 'task_function'")

        return _wrap_task_function(task_module.task_function, task_name)
    except Exception as e:
        _send_error_notification("load_task_function", str(e))
        logger.error(f"Failed to load task function: {e}")
        raise


def _install_dependencies(task_name: str, requirements_path: str) -> None:
    """Install task dependencies from requirements file.

    Args:
        task_name (str): Name of the task.
        requirements_path (str): Path to requirements file.
    """
    try:
        logger.info(f"Installing dependencies for task {task_name}...")
        import subprocess
        subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', requirements_path])
        logger.info(f"Dependencies for task {task_name} installed successfully.")
    except Exception as e:
        _send_error_notification("_install_dependencies", str(e))
        logger.error(f"Failed to install dependencies for task {task_name}: {e}")
        raise


def initialize_scheduler(config: dict) -> BackgroundScheduler:
    """Initialize scheduler with tasks from config.

    Args:
        config (dict): Configuration dictionary.

    Returns:
        BackgroundScheduler: Initialized scheduler instance.
    """
    try:
        for task in config['tasks']:
            try:
                # 检查任务是否开启
                if not task.get('enabled', True):
                    logger.info(f"Task {task['name']} is disabled, skipping...")
                    continue

                task_function = load_task_function(task['name'])
                trigger = _create_trigger(task)
                if trigger:
                    scheduler.add_job(
                        task_function,
                        trigger=trigger,
                        args=[task['name']],
                        id=task['name'],
                        max_instances=1
                    )
                    logger.info(f"Task {task['name']} loaded successfully.")
            except Exception as e:
                logger.error(f"Failed to load task {task['name']}: {e}")
                _send_error_notification("initialize_scheduler", str(e))
        return scheduler
    except Exception as e:
        _send_error_notification("initialize_scheduler", str(e))
        logger.error(f"Failed to initialize scheduler: {e}")
        raise


def _create_trigger(task: dict) -> object:
    """Create trigger based on task configuration.

    Args:
        task (dict): Task configuration.

    Returns:
        object: Trigger object or None if invalid trigger type.
    """
    trigger_type = task['trigger']
    if trigger_type == 'cron':
        return CronTrigger.from_crontab(task['cron_expression'])
    elif trigger_type == 'interval':
        return IntervalTrigger(seconds=task['interval'])
    elif trigger_type == 'date':
        return DateTrigger(run_date=task['run_date'])
    return None


def monitor_config_file(shutdown_event: threading.Event) -> None:
    """Monitor config file for changes and reload tasks if modified.

    Args:
        shutdown_event (threading.Event): Event to signal shutdown.
    """
    try:
        config_path = os.path.join(CONFIG_DIR, CONFIG_FILE)
        last_modified = os.path.getmtime(config_path)

        while not shutdown_event.is_set():
            try:
                current_modified = os.path.getmtime(config_path)
                if current_modified > last_modified:
                    logger.info("Config file modified, reloading tasks...")
                    reload_tasks()
                    last_modified = current_modified
            except Exception as e:
                logger.error(f"Failed to monitor config file: {e}")
                _send_error_notification("monitor_config_file", str(e))
            shutdown_event.wait(CONFIG_CHECK_INTERVAL)
    except Exception as e:
        _send_error_notification("monitor_config_file", str(e))
        logger.error(f"Failed to monitor config file: {e}")
        raise


def reload_tasks() -> None:
    """Reload all tasks from config file."""
    try:
        global config
        new_config = load_config()
        if new_config != config:
            scheduler.remove_all_jobs()
            initialize_scheduler(new_config)
            config = new_config
    except Exception as e:
        logger.error(f"Failed to reload config: {e}")
        _send_error_notification("reload_tasks", str(e))


def main() -> None:
    """Main function to start the scheduler."""
    try:
        # 加载配置
        global config
        config = load_config()

        # 初始化调度器
        scheduler = initialize_scheduler(config)
        scheduler.start()

        # 创建关闭事件
        shutdown_event = threading.Event()
        # 启动配置文件监控线程
        monitor_thread = threading.Thread(target=monitor_config_file, args=(shutdown_event,))
        monitor_thread.start()

        try:
            # 主循环，等待关闭事件
            while not shutdown_event.is_set():
                shutdown_event.wait(1)
        except (KeyboardInterrupt, SystemExit):
            # 处理中断信号，优雅关闭
            logger.info("Shutting down scheduler...")
            scheduler.shutdown()
            shutdown_event.set()
    except Exception as e:
        _send_error_notification("main", str(e))
        logger.error(f"Failed to start scheduler: {e}")
        raise


def _wrap_task_function(task_func: callable, task_name: str) -> callable:
    """Wrap task function with execution time monitoring and error handling.

    Args:
        task_func (callable): Original task function.
        task_name (str): Name of the task.

    Returns:
        callable: Wrapped task function.
    """

    def wrapped_function(*args, **kwargs):
        start_time = time.time()
        try:
            result = task_func(*args, **kwargs)
            execution_time = time.time() - start_time
            if execution_time > 60:
                logger.warning(f"Task {task_name} took {execution_time:.2f} seconds to complete")
            return result
        except Exception as e:
            logger.error(f"Task {task_name} failed: {str(e)}")
            task_func.fail_count = getattr(task_func, 'fail_count', 0) + 1
            if task_func.fail_count >= 3:
                logger.error(f"Task {task_name} failed 3 times, disabling it")
                _send_error_notification(task_name, "任务出现异常超过三次，已终止该任务执行")
                scheduler.remove_job(task_name)
            # 新增异常通知逻辑
            _send_error_notification(task_name, str(e))

    return wrapped_function


def _send_error_notification(task_name: str, error_message: str) -> None:
    """Send error notification based on configuration.

    Args:
        task_name (str): Name of the failed task.
        error_message (str): Error message to send.
    """
    try:
        from Tools.notify import load_config, send_email, send_wxpusher_by_uids, send_wxpusher_by_topic, \
            _send_notification
        from Tools.ipGeo import get_ip_and_geo
        global _ip_geo_cache, _last_cache_time
        # 仅在 isNeedIp 为 True 时获取并添加 IP 信息
        # 检查缓存是否过期（每天更新一次）
        current_time = time.time()
        if _ip_geo_cache is None or _last_cache_time is None or (current_time - _last_cache_time) >= 86400:
            _ip_geo_cache = get_ip_and_geo()
            _last_cache_time = current_time
        ip_info = f"{_ip_geo_cache}"
        subject=f"pyTaskScheduler调度出现异常-{hostName}"
        message = f"<h1>{subject}</h1><h3>hostName：</h3>[{hostName}]<br><h3>Task:</h3>[ {task_name}] <br><h3>failed with error: </h3>{error_message}</h3><br><h3>Source:</h3>{ip_info}"
        _send_notification("error_notification", subject, message)
        logger.info(f"{task_name}:Error notification sent request post successfully!")
    except Exception as e:
        logger.error(f"{task_name}:Failed to send  error notification: {e}")


if __name__ == "__main__":
    # 程序入口
    main()
